/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#include "pch.h"
#include <journal/views/page_curl.h>
#include <journal/common/util.h>

namespace journal { namespace view {

// Helper macro for a straight line F(x) that passes through {x1, y1} and {x2, y2}.
// We can't make this a template function (C++ doesn't let you have float literals
// as template parameters).
#define STRAIGHT_LINE(x1, y1, x2, y2, x) (((y2 - y1) / (x2 - x1)) * (x - x1) + y1)

page_curl::page_curl() : 
        _pageNode(nullptr)
{
    this->reset();
}

std::shared_ptr<page_curl> page_curl::create()
{
    return std::make_shared<page_curl>();
}

void page_curl::reset()
{
    _pointerState = pointerState::none;
    _numTicksInState = 0;
    _userCurl = false;
    _autoCurl = false;

    _currentCone.theta = 90.0f;
    _currentCone.ay = -70.0f;
    _currentCone.angle = 0.0f;
    _currentCone.conicContribution = 1.0f;

    _prevX[0] = _prevX[1] = -9999.0f;
    _prevY[0] = _prevY[1] = -9999.0f;
    _prevTimestamp[0] = _prevTimestamp[1] = -1.0f;
    _userCurlingRight = false;

    if (_pageNode != nullptr)
    {
        // TRACK: remove this and do it properly
        std::shared_ptr<g::imaterial> material = _pageNode->getMaterial();
        _pageNode->reset();
        _pageNode->getMaterial()->setTexture(material->getTexture());
        _pageNode->getMaterial()->setSpecularPower(FLT_MAX); // matte
        _pageNode->getMaterial()->setEmissiveColor(g::packColor(255, 255, 255, 255));
        _pageNode->getMaterial()->setDiffuseColor(g::packColor(255, 100, 100, 100));

        _pageNode->setPosition(m::vector3d(0, 0, 0));
        _pageNode->setDefaultEffect(g::standard_effect::meshPhong);
    }
    _pageNode = nullptr;
}

// Debug helper call, called by pressing the hotkey for curling
void page_curl::debugCurlPage(cone_parameters cone)
{
    BAJA_VERIFY(this->pageIsAttached());
    this->curlPage(_pageNode, cone);
}

// Called by page::onRender() for each frame. This reads _userCurl || _autoCurl to see if it should
// curl the page with the parameters _currentCone.
void page_curl::onRender()
{
    // Read state under a lock
    cone_parameters nextCone;
    cone_parameters currentCone;
    bool userCurl;
    bool autoCurl;
    {
        nextCone = _nextCone;
        currentCone = _currentCone;
        userCurl = _userCurl;
        autoCurl = _autoCurl;
    }

    // Smooth going from currentCone to nextCone
    cone_parameters cone;
    b::float32 dt = nextCone.theta - currentCone.theta;
    b::float32 da = nextCone.ay - currentCone.ay;
    b::float32 dr = nextCone.angle - currentCone.angle;
    b::float32 dc = nextCone.conicContribution - currentCone.conicContribution;
    b::float32 distance = sqrt(dt * dt + da * da + dr * dr + dc * dc);

    b::float32 maxDistance = constants::general::maxCurlDistance();
    if (distance < maxDistance)
    {
        cone = nextCone;
    }
    else
    {
        b::float32 scale = maxDistance / distance;
        cone.theta = currentCone.theta + scale * dt;
        cone.ay = currentCone.ay + scale * da;
        cone.angle = currentCone.angle + scale * dr;
        cone.conicContribution = currentCone.conicContribution + scale * dc;
    }

    if (userCurl || autoCurl)
    {
        this->curlPage(_pageNode, cone);
        {
            _currentCone = cone;
        }
    }

    if (autoCurl)
    {
        this->startAutoCurl(false);
    }
}

void page_curl::attachPage(const std::shared_ptr<paper_sheet_node> &pageNode, bool lastPage, paper_type paperType)
{
    _pageNode = pageNode;

    _pageMaxX = (_pageNode->width() - _pageNode->offset()) / 2.0f;
    _pageMinX = -_pageMaxX;
    _pageMaxY = (_pageNode->height() - _pageNode->offset()) / 2.0f;
    _pageMinY = -_pageMaxY;
    _pageCoordTransform = _pageNode->offset();

    _lastPage = lastPage;

    // reset direction history
    _prevX[0] = _prevX[1] = -9999.0f;
    _prevY[0] = _prevY[1] = -9999.0f;
    _prevTimestamp[0] = _prevTimestamp[1] = -1.0f;

    _paperType = paperType;
}

void page_curl::attachPage(const std::shared_ptr<ipage> &pageView, bool lastPage, paper_type paperType) 
{
    const std::shared_ptr<paper_sheet_node> &pageNode = 
        std::static_pointer_cast<paper_sheet_node>(pageView->sceneNode());

    this->attachPage(pageNode, lastPage, paperType);
}

bool page_curl::shouldHideThisPage()
{
    return (!_userCurlingRight || _autoCurl) && _currentCone.angle > constants::general::pageCurlEndAngle();
}

b::float32 page_curl::alphaValue()
{
    b::float32 alpha = STRAIGHT_LINE(
        constants::general::pageCurlTransparencyStartAngle(), 
        1.0f, 
        constants::general::pageCurlEndAngle(), 
        0.0f, 
        _currentCone.angle
        );

    alpha = min(alpha, 1.0f);
    alpha = max(alpha, 0.0f);
    return alpha;
}

// curling progress from 0 to 1 where 1 is out of view and 0 is flat on the XY plane
b::float32 page_curl::progress()
{
    if (!_userCurl && !_autoCurl) return 0;

    return min(1, max(0, _currentCone.angle / constants::general::pageCurlEndAngle()));
}

// Starts automatically uncurling the page. This just sets the right {_theta,
// _ay, _angle} for the next frame of the curl. It stops automatically rendering
// when the page is fully uncurled.
void page_curl::startAutoCurl(bool firstTime)
{
    BAJA_VERIFY(this->pageIsAttached());

    // Read state under a lock
    cone_parameters cone;
    bool userCurl;
    bool autoCurl;
    {
        cone = _currentCone;
        userCurl = _userCurl;
        autoCurl = _autoCurl;
    }

    bool doneCurlBackwards = ((cone.theta > 89.999f) && (cone.ay < -69.999f) && (cone.angle < 0.001f) && (abs(cone.conicContribution) > 0.999f));
    bool doneCurlForwards = ((cone.angle > 99.999f));
    bool doneCurling = doneCurlBackwards || doneCurlForwards;
    userCurl = false;
    autoCurl = !doneCurling;

    if (!doneCurling)
    {
        b::float32 curTime = constants::general::currentTime();
        b::float32 timeDiff = curTime - _autoCurlStartTime;
        if (firstTime)
        {
            _autoCurlStartTime = curTime;
            timeDiff = 0.0f;
        }

        // TRACK 498: tune all this for inertia
        
        if (cone.angle > 40 && !_lastPage)
        {
            cone.angle = min(cone.angle + timeDiff*timeDiff/200000.0f, 100.0f);
            cone.theta = min(cone.theta + timeDiff*timeDiff/800000.0f, 50.0f);
        }
        else
        {
            cone.angle = max(cone.angle - timeDiff*timeDiff/300000.0f, 0.0f);
            cone.theta = min(cone.theta + timeDiff*timeDiff/100000.0f, 90.0f);
            cone.ay = max(cone.ay - timeDiff*timeDiff/200000.0f, -70.0f);
        }

        if (cone.conicContribution > 0.0)
        {
            cone.conicContribution = min(cone.conicContribution + timeDiff*timeDiff/100000.0f, 1.0f);
        }
        else
        {
            cone.conicContribution = max(cone.conicContribution - timeDiff*timeDiff/100000.0f, -1.0f);
        }
    }

    // Write state under a lock
    {
        _nextCone = cone;
        _userCurl = userCurl;
        _autoCurl = autoCurl;
    }
}

// Given {x, y} on the coordinate system of the page (_pageMin[X,Y] to _pageMax[X,Y]),
// this generates appropriate curl parameters {_theta, _ay, _angle, _conicContribution} 
// and sets _userCurl to true. It also halts any auto curling that might be happening.
void page_curl::startUserCurl(b::float32 x, b::float32 y)
{
    BAJA_VERIFY(this->pageIsAttached());
    
    // Scale x & y so they are parametrized to normal ranges

                                    // x ranges from -Xmax to Xmax   (lhs to rhs)
    x = x / (2.0f * _pageMaxX);     // x ranges from -0.5 to 0.5     (lhs to rhs)
    x = x + 0.5f;                   // x ranges from 0 to 1          (lhs to rhs)
    x = min(x, 1.0f);               // x ranges from 1 to 0          (rhs to lhs)
    x = max(x, 0.0f);

                                    // y ranges from -Ymax to Ymax   (bottom to top)
    y = y / (_pageMaxY);            // y ranges from -1 to 1         (bottom to top)
    y = min(y, 1.0f);
    y = max(y, -1.0f);

    // Transform {x, y} to {theta, ay, angle, conicContribution}

    page_curl::cone_parameters cone;
    switch (_paperType)
    {
    case paper_type::looseLeaf:
        cone = this->paperParams(x, y);
        break;		
    case paper_type::plasticCover:
        cone = this->plasticParams(x, y);
        break;
    default:
        BAJA_VERIFY(false);
    }

    {
        // keep some history so we know if we're going left or right.
        // TRACK 498: use this for inertia when doing auto curling.

        if (_prevTimestamp[0] < 0.0f)
        {
            // history is not available, so just make it seem like
            // we've been at the same point for a while

            _userCurlingRight = true;   // hack to make backwards curling not flicker
            _prevX[0] = _prevX[1] = x;
            _prevY[0] = _prevY[1] = y;
            _prevTimestamp[0] = _prevTimestamp[1] = constants::general::currentTime();
            _currentCone = cone;
        }
        else
        {
            _userCurlingRight = (x + 0.001) > _prevX[0];

            _prevX[1] = _prevX[0];
            _prevY[1] = _prevY[0];
            _prevTimestamp[1] = _prevTimestamp[0];

            _prevX[0] = x;
            _prevY[0] = y;
            _prevTimestamp[0] = constants::general::currentTime();
        }

        _nextCone = cone;
        _userCurl = true;
        _autoCurl = false;
    }
}

page_curl::cone_parameters page_curl::paperParams(b::float32 x, b::float32 y)
{
    BAJA_VERIFY(abs(x) < 1.001 && abs(y) < 1.001);
    b::float32 theta, ay, angle;

    if (x > 0.95f)
    {
        theta = STRAIGHT_LINE(1.0f,    90.0f,   0.95f,  60.0f, x);
        ay =    STRAIGHT_LINE(1.0f,   -20.0f,   0.95f,  -5.0f, x);
        angle = 0.0;
    }
    else if (x > 0.8333f)
    {
        theta = STRAIGHT_LINE(0.95f,   60.0f, 0.8333f,  55.0f, x);
        ay =    STRAIGHT_LINE(0.95f,   -5.0f, 0.8333f,  -4.0f, x);
        angle = STRAIGHT_LINE(0.95f,    0.0f, 0.8333f,  13.0f, x);
    }
    else if (x > 0.3333f)
    {
        theta = STRAIGHT_LINE(0.8333f, 55.0f, 0.3333f,  45.0f, x);
        ay =    STRAIGHT_LINE(0.8333f, -4.0f, 0.3333f,  -10.0f, x);
        angle = STRAIGHT_LINE(0.8333f, 13.0f, 0.3333f,  35.0f, x);
    }
    else if (x > 0.1666f)
    {
        theta = STRAIGHT_LINE(0.3333f, 45.0f, 0.1666f,  25.0f, x);
        ay =    STRAIGHT_LINE(0.3333f, -10.0f, 0.1666f, -30.0f, x);
        angle = STRAIGHT_LINE(0.3333f, 35.0f, 0.1666f,  60.0f, x);
    }
    else
    {
        theta = STRAIGHT_LINE(0.1666f, 25.0f,    0.0f,  20.0f, x);
        ay =    STRAIGHT_LINE(0.1666f, -30.0f,   0.0f, -40.0f, x);
        angle = STRAIGHT_LINE(0.1666f, 60.0f,    0.0f,  95.0f, x);
    }
    
    page_curl::cone_parameters cp(theta, ay, angle, y);
    return cp;
}

page_curl::cone_parameters page_curl::plasticParams(b::float32 x, b::float32 y)
{
    BAJA_VERIFY(abs(x) < 1.001 && abs(y) < 1.001);
    b::float32 theta, ay, angle;

    if (x > 0.95f)
    {
        theta = STRAIGHT_LINE(1.0f,  90.0f, 0.9f,  40.0f, x);
        ay =    STRAIGHT_LINE(1.0f, -30.0f, 0.9f, -20.0f, x);
        angle = 0.0;
    }
    else
    {
        theta = STRAIGHT_LINE(0.95f,  40.0f, 0.0f,  35.0f, x);
        ay =    STRAIGHT_LINE(0.95f, -20.0f, 0.0f, -25.0f, x);
        angle = STRAIGHT_LINE(0.95f,   0.0f, 0.0f,  95.0f, x);
    }

    page_curl::cone_parameters cp(theta, ay, angle, y);
    return cp;
}

// This is the meat. Given the four cone input parameters, map all the vertices of the page to look
// like the pages are turning. The idea is from a brilliant paper: "Turning Pages of 3D Electronic 
// Books" by Hong et al, PARC. IEEE Symposium on 3d User Interfaces 2006.
//      http://www.parc.com/content/attachments/turning-pages-3D.pdf
//
// In the paper they show how mapping a sheet of paper to a cone whose side is on the spine of the 
// book looks just like curling a page by its corner. The cone's parameters are theta (angle of a
// right cone) and Ay (the location on the y-axis of the apex of the cone... this value is always
// negative). Then, as the page turns, they rotate the whole piece of paper by 'angle'. This turns a 
// page by the top right or bottom right... but what about turning the page by the middle right? 
//
// I'm extending their idea by saying that if you're turning the page from the middle right 
// (conicContribution = 0.0) then we wrap the page around a cylinder whose side is on the axis
// of the book. If you turn the page from the top right (conicContribution = 1.0) then we wrap 
// the page around a cone whose apex is 'south' of the page. If you turn the page from the bottom 
// right (conicContribution = -1.0), then we wrap the page around a cone whose apex is 'north' of 
// the page. 
//
// Anywhere in between, and the info from two sources is combined linearly. Eg, of you turn the 
// page from 3/4 of the way up the page (conicContribution = 0.75) then we take half the 'south'
// cone's data and half the cylinder's data to show the page.
void page_curl::curlPage(const std::shared_ptr<paper_sheet_node> &pageNode, cone_parameters cone)
{
    b::float32 theta = cone.theta;
    b::float32 Ay = cone.ay;
    b::float32 angle = cone.angle;
    b::float32 conicContribution = cone.conicContribution;

    theta = m::degToRad(theta);
    angle = m::degToRad(angle);

    std::shared_ptr<g::ivertex_buffer> vertexBuffer = pageNode->vertexBuffer();
    g::vertex_lit_colored_textured* buffer = nullptr;

    vertexBuffer->lockNoOverwrite(
        0,
        pageNode->vertexCountX() * pageNode->vertexCountY(),
        (b::memory**)&buffer
        );

    b::int32 iMax = pageNode->vertexCountX();
    b::int32 jMax = pageNode->vertexCountY();

    // As the user grabs towards the middle-right of the page, we curl the paper by deforming it
    // on to a cylinder. The cylinder radius is taken as the endpoint of the cone parameters:
    // ie: cylRadius = R*sin(theta) where R is the distance to the rightmost point on the page, all the way up.
    b::float32 cylR = sqrt(iMax * iMax + (jMax/2 - Ay)*(jMax/2 - Ay));
    b::float32 cylRadius = cylR * sin(theta);

    // This is a hack to fix the bump at the start of a curl. If theta is greater than 50 degrees,
    // cylRadius needs to start approaching "infinity".
    b::float32 cylRadiusAt50 = cylR * sin(m::degToRad(50));
    if (theta > m::degToRad(50.0f))
    {
        b::float32 x1 = m::degToRad(90.0f);
        b::float32 x2 = m::degToRad(50.0f);
        cylRadius = STRAIGHT_LINE(x1, 200.0f, x2, cylRadiusAt50, theta);
    }

    // Flipping from top corner or bottom corner?
    b::float32 posNegOne;
    if (conicContribution > 0) 
    {
        // flipping from top corner
        posNegOne = 1.0f;
    } 
    else 
    {
        // flipping from bottom corner
        posNegOne = -1.0f;
        Ay = -Ay + pageNode->vertexCountY();
    }

    conicContribution = abs(conicContribution);

    for (b::int32 j = 0; j < jMax; j++)
    {
        // Vectorization notes:
        //    * i & j need to stay as int32. Converts from uint32 to float aren't supported in the V1 vectorizer.
        //    * we don't scalar expand IV's from outer loops -- just a stupid NYI in V1 vectorizer.
        //    * the member variables need to be hoisted. Again, V1 vectorizer.
        b::int32 jOffset = j * pageNode->vertexCountX();
        float jFloat = (float)j;
        float pageMaxX = _pageMaxX;
        float pageMaxY = _pageMaxY;
        float pageCoordTransform = _pageCoordTransform;

        for (b::int32 i = 0; i < iMax; i++)
        {
            // Be careful with units here. i & j range from [0, iMax] & [0, jMax] which are just the vertex indices.
            // 'ay' is in the coordinate system of the vertex indices. The actual values of the vertices are in 
            // page units (pageMin[X,Y], pageMax[X,Y]).

            float x = (float)i;
            float y = jFloat;
            float z = 0;

            float coneX = x;
            float coneY = y;
            float coneZ = z;
            {
                // Compute conical parameters
                float R = sqrt(x * x + (y - Ay)*(y - Ay));
                float r = R * sin(theta);
                float beta  = asin(x / R) / sin(theta);
            
                // Cone it up
                coneX = r * sin(beta);
                coneY = R + posNegOne * Ay - r * (1 - cos(beta)) * sin(theta);
                coneZ = r * (1 - cos(beta)) * cos(theta);
          
                // Then rotate by angle about the y axis
                coneX = coneX * cos(angle) - coneZ * sin(angle);
                coneZ = coneX * sin(angle) + coneZ * cos(angle);

                // Transform coordinates to the page
                coneX = (coneX * pageCoordTransform) - pageMaxX;
                coneY = (-posNegOne * coneY * pageCoordTransform) + pageMaxY;
                coneZ = coneZ * pageCoordTransform;
            }

            float cylX = x;
            float cylY = y;
            float cylZ = z;
            {
                float beta = cylX / cylRadius;

                // Rotate (0,0,0) by beta around line given by x = 0, z = cylRadius.
                // aka Rotate (0,0,-cylRadius) by beta, then add cylRadius back to z coordinate
                cylZ = -cylRadius;
                cylX = -cylZ * sin(beta);
                cylZ = cylZ * cos(beta);
                cylZ += cylRadius;

                // Then rotate by angle about the y axis
                cylX = cylX * cos(angle) - cylZ * sin(angle);
                cylZ = cylX * sin(angle) + cylZ * cos(angle);

                // Transform coordinates to the page
                cylX = (cylX * pageCoordTransform) - pageMaxX;
                cylY = (-cylY * pageCoordTransform) + pageMaxY;
                cylZ = cylZ * pageCoordTransform;
            }

            // combine cone & cylinder systems
            x = conicContribution * coneX + (1-conicContribution) * cylX;
            y = conicContribution * coneY + (1-conicContribution) * cylY;
            z = conicContribution * coneZ + (1-conicContribution) * cylZ;

            buffer[jOffset + i].position.x = x;
            buffer[jOffset + i].position.y = y;
            buffer[jOffset + i].position.z = z;
        }
    }

    vertexBuffer->unlock();

    pageNode->updateNormals();
}

}} // namespace journal::view